Go语言竞争状态 图片看不了?点击切换HTTP 返回上层
Go语言中如果两个或者多个 goroutine 在没有互相同步的情况下,访问某个共享的资源,并试图同时读和写这个资源,就处于相互竞争的状态,这种情况被称作竞争状态(race candition)。
竞争状态的存在是让并发程序变得复杂的地方,十分容易引起潜在问题。对一个共享资源的读和写操作必须是原子化的,换句话说,同一时刻只能有一个 goroutine 对共享资源进行读和写操作。
【示例】包含竞争状态的示例程序。

图:竞争状态下程序行为的图像表达
每个 goroutine 都会覆盖另一个 goroutine 的工作。这种覆盖发生在goroutine 切换的时候。每个 goroutine 创造了一个 counter 变量的副本,之后就切换到另一个 goroutine。当这个 goroutine 再次运行的时候,counter 变量的值已经改变了,但是 goroutine 并没有更新自己的那个副本的值,而是继续使用这个副本的值,用这个值递增,并存回 counter 变量,结果覆盖了另一个 goroutine 完成的工作。
代码说明如下:
这个函数在第 43 行调用了 runtime 包的 Gosched 函数,用于将 goroutine 从当前线程退出,给其他 goroutine 运行的机会。在两次操作中间这样做的目的是强制调度器切换两个 goroutine,以便让竞争状态的效果变得更明显。
Go语言有一个特别的工具,可以在代码里检测竞争状态。在查找这类错误的时候,这个工具非常好用,尤其是在竞争状态并不像这个例子里这么明显的时候。让我们用这个竞争检测器来检测一下我们的示例代码,代码如下所示。
竞争状态的存在是让并发程序变得复杂的地方,十分容易引起潜在问题。对一个共享资源的读和写操作必须是原子化的,换句话说,同一时刻只能有一个 goroutine 对共享资源进行读和写操作。
【示例】包含竞争状态的示例程序。
// 这个示例程序展示如何在程序里造成竞争状态
// 实际上不希望出现这种情况
package main
import (
"fmt"
"runtime"
"sync"
)
var (
// counter 是所有goroutine 都要增加其值的变量
counter int
// wg 用来等待程序结束
wg sync.WaitGroup
)
// main 是所有Go 程序的入口
func main() {
// 计数加 2,表示要等待两个goroutine
wg.Add(2)
// 创建两个goroutine
go incCounter(1)
go incCounter(2)
// 等待 goroutine 结束
wg.Wait()
fmt.Println("Final Counter:", counter)
}
// incCounter 增加包里counter 变量的值
func incCounter(id int) {
// 在函数退出时调用Done 来通知main 函数工作已经完成
defer wg.Done()
for count := 0; count < 2; count++ {
// 捕获 counter 的值
value := counter
// 当前 goroutine 从线程退出,并放回到队列
runtime.Gosched()
// 增加本地value 变量的值
value++
// 将该值保存回counter
counter = value
}
}
输出结果如下所示。Final Counter: 2
变量 counter 会进行 4 次读和写操作,每个 goroutine 执行两次。但是,程序终止时,counter 变量的值为 2。下图提供了为什么会这样的线索。
图:竞争状态下程序行为的图像表达
每个 goroutine 都会覆盖另一个 goroutine 的工作。这种覆盖发生在goroutine 切换的时候。每个 goroutine 创造了一个 counter 变量的副本,之后就切换到另一个 goroutine。当这个 goroutine 再次运行的时候,counter 变量的值已经改变了,但是 goroutine 并没有更新自己的那个副本的值,而是继续使用这个副本的值,用这个值递增,并存回 counter 变量,结果覆盖了另一个 goroutine 完成的工作。
代码说明如下:
- 在第 25 行和第 26 行,使用 incCounter 函数创建了两个 goroutine。
- 在第 34 行,incCounter 函数对包内变量 counter 进行了读和写操作,而这个变量是这个示例程序里的共享资源。每个 goroutine 都会先读出这个 counter 变量的值。
- 在第 40 行将 counter 变量的副本存入一个叫作 value 的本地变量。
- 在第 46 行,incCounter 函数对 value 的副本的值加 1。
- 在第 49 行将这个新值存回到 counter 变量。
这个函数在第 43 行调用了 runtime 包的 Gosched 函数,用于将 goroutine 从当前线程退出,给其他 goroutine 运行的机会。在两次操作中间这样做的目的是强制调度器切换两个 goroutine,以便让竞争状态的效果变得更明显。
Go语言有一个特别的工具,可以在代码里检测竞争状态。在查找这类错误的时候,这个工具非常好用,尤其是在竞争状态并不像这个例子里这么明显的时候。让我们用这个竞争检测器来检测一下我们的示例代码,代码如下所示。
go build -race // 用竞争检测器标志来编译程序
./example // 运行程序
==================
WARNING: DATA RACE
Write by goroutine 5:
main.incCounter()
/example/main.go:49 +0x96
Previous read by goroutine 6:
main.incCounter()
/example/main.go:40 +0x66
Goroutine 5 (running) created at:
main.main()
/example/main.go:25 +0x5c
Goroutine 6 (running) created at:
main.main()
/example/main.go:26 +0x73
==================
Final Counter: 2
Found 1 data race(s)
上述代码中的竞争检测器指出了 4 行代码有问题,如下所示。
Line 49: counter = value
Line 40: value := counter
Line 25: go incCounter(1)
Line 26: go incCounter(2)